#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <ctype.h>
 
#define LEFT "["
#define RIGHT "]"
#define DELIM " \t" //忽视tab建
#define LINE_SIZE 1024
#define LABLE "#"
#define ARGV_SIZE 32
#define EXIT_CODE 44
#define OUT_REDIR 0
#define APPEND_REDIR 1
#define IN_REDIR 2 
#define NONE -1

int rdir = 0; //判断重定向类型
int lastcode = 0;
int quit = 0;
char *argv[ARGV_SIZE];
char commandline[LINE_SIZE];
char pwd[LINE_SIZE];
const char **environ;
char myenv[LINE_SIZE];
char *rdirfilename;


const char *getusername()
{
	return getenv("USER");
}

const char *gethostname()
{
	return getenv("HOSTNAME");
}

void getpwd()
{
	getcwd(pwd, sizeof(pwd));
}

void check_redir(char *cmd)
{
	char *pos = cmd; //避免commandline被破坏
	
	while(*pos)
	{
		if(*pos == '>') 
		{
			if(*(pos + 1) == '>') //追加重定向
			{
				*pos++ = '\0';
				*pos++ = '\0';
				while(isspace(*pos)) pos++;
				rdirfilename = pos;
				rdir = APPEND_REDIR;
				break;
			}
			else //输出重定向
			{
				*pos++ = '\0';
				while(isspace(*pos)) pos++;
				rdirfilename = pos;
				rdir = OUT_REDIR;
				break;	
			}
		}
		else if(*pos == '<') //输入重定向 
		{
			*pos++ = '\0';
			while(isspace(*pos)) pos++;
			rdirfilename = pos;
			rdir = IN_REDIR;
			break;
		}
		else
		{
			//do nothing
		}
	
		pos++;
	}
} 

char* interact(char *cline, int size)
{       
         //更新当前路径
         getpwd();
         printf(LEFT"%s@%s:~%s"RIGHT""LABLE" " , getusername(), gethostname(), pwd);
         //从缓冲区读取数据到cline中
         char *s = fgets(cline, size, stdin);
         assert(s);
         
         //定义变量而不使用，编译器可能会报错
         //为了避免编译器报错，这里我们需要使用s
         (void)s;
 
         //输入“abcd\n\0”,读取缓冲区数据时，肯定会读取到\n,我们不需要\n
         if(s) cline[strlen(cline) - 1] = '\0';

	 check_redir(cline); 
}
 
 //2.子串分割问题
 int splitstring(char *cline, char *argv[])
 {
         int i = 0;
         argv[i++] = strtok(cline, DELIM);
         while(argv[i++] = strtok(NULL, DELIM)); //接着第一次的继续切割
 
         return i - 1; //最后i会多加一次
 }

//普通命令的执行需要fork创建子进程
void NormalExcute(char *argv[])
{
	pid_t id = fork();
	if(id == 0) //child
	{
		int fd = 0;
		//execvpe(argv[0], argv, environ); 也可以不使用execvpe因为子进程会继承父进程的路径
		//让子进程执行命令

		//在进行重定向的时候，进程替换不会影响到重定向？
		if(rdir == IN_REDIR)
		{
			fd = open(rdirfilename, O_RDONLY);
			dup2(fd, 0);
		}
		else if(rdir == OUT_REDIR)
		{
			fd = open(rdirfilename, O_WRONLY|O_CREAT|O_TRUNC, 0666); //可以设置权限
			dup2(fd, 1);
		}
		else if(rdir == APPEND_REDIR)
		{
			fd = open(rdirfilename, O_WRONLY|O_CREAT|O_APPEND, 0666);
			dup2(fd, 1);
		}


		execvp(argv[0], argv);
		exit(EXIT_CODE);	
	}
	else if(id >  0) //parent
	{	
		int status = 0; //输出型参数
		pid_t rid = waitpid(id,  &status, 0); //WONHANG 非等待轮询
		
		//阻塞等待
		if(id == rid)	
		{
			lastcode = WEXITSTATUS(status);
		}
	}
	else //error
	{
		perror("Normal::Excute");
		return;
	}
}
 
//内建命令的处理
int bulidCommand(char *argv[], int argc)
{
	if(argc == 2 && strcmp(argv[0], "cd") == 0)
	{
		chdir(argv[1]); //系统调用接口chdir更改当前路径
		getpwd();//刷新pwd中路径
		sprintf(getenv("PWD"), "%s", pwd); //修改环境变量PWD的值
		return 1;
	}
	else if(argc == 2 && strcmp(argv[0], "export") == 0)
	{
		//往环境变量表中导入新元素时并不是将环境变量的value值拷贝进表中而是将指向它的指针放入表中
		//所以我们需要将环境变量单独放在一个空间中，避免它的数据被覆盖（commandline）
		strcpy(myenv, argv[1]);		
		putenv(myenv);
		return 1;
	}
	else if(argc == 2 && strcmp(argv[0], "echo") == 0)
	{
		if(argv[1] == "$?")
		{
			printf("%d\n", lastcode); //返回上一次的退出码
			lastcode = 0; 
		}
		else if(*argv[1] == '$')
		{
			char *val = getenv(argv[1] + 1);
			if(val) printf("%s\n", val);   //输除环境变量的值
		}
		else
		{
			printf("%s\n", argv[1]); //直接输出
		}

		return 1;
	}

	if(argc == 1 && strcmp(argv[0], "ls")) //普通命令
	{
		argv[argc++] = "--color";
		argv[argc] = NULL;
	}
	
	return 0;
}


 int main()
 {       

	while(!quit)
	{
	 	 rdir = NONE;
		 rdirfilename = NULL;  //每次输出命令都需要清空rdirfilename

	         //1.交互问题 获取命令行
	         interact(commandline, sizeof(commandline));
	         
	         //debug
	         //2.子串分割问题，解析命令行
	         int argc = splitstring(commandline, argv);
		 if(argc == 0) continue;
	         
	         //3.指令的判断 内建命令和普通命令
		 //内建命令
	         int n = bulidCommand(argv, argc);
	
	       	if(!n) NormalExcute(argv);
		 //普通命令
	}
	 return 0;
}
